//! Expression constructs used in Roc's canonicalization phase.
//!
//! This module defines the `Expr` union which represents all possible expressions
//! in Roc's Canonical Intermediate Representation (CIR). These expressions are
//! created during the canonicalization phase and represent the semantic meaning
//! of parsed code after semantic analysis.
//!
//! Expression types include:
//! - Literals: numbers, strings, lists, records, tuples
//! - Operations: function calls, binary operations, field access
//! - Control flow: if expressions, match expressions, blocks
//! - Variables: local lookups, external lookups (defined in another motdule)
//! - Advanced: lambdas, tags, error handling
//!
//! Examples of expressions:
//! - `42` - integer literal
//! - `"hello"` - string literal
//! - `[1, 2, 3]` - list literal
//! - `{ name: "Alice", age: 30 }` - record literal
//! - `add(5, 3)` - function call
//! - `if x > 0 "positive" else "non-positive"` - conditional

const std = @import("std");
const base = @import("base");
const types = @import("types");
const collections = @import("collections");
const builtins = @import("builtins");

const ModuleEnv = @import("ModuleEnv.zig");
const CIR = @import("CIR.zig");

const StringLiteral = base.StringLiteral;
const Region = base.Region;
const DataSpan = base.DataSpan;
const CalledVia = base.CalledVia;
const Ident = base.Ident;
const SExprTree = base.SExprTree;
const SExpr = base.SExpr;
const RocDec = builtins.dec.RocDec;
const TypeVar = types.Var;

const Self = Expr;

/// An expression in the Roc language.
pub const Expr = union(enum) {
    /// An number literal with a specific value.
    /// Represents whole numbers in various bases (decimal, hex, octal, binary).
    ///
    /// ```roc
    /// 42          # Decimal number
    /// 0xFF        # Hexadecimal integer
    /// 0o755       # Octal integer
    /// 0b1010      # Binary integer
    /// 42u8        # Decimal number with type suffix
    /// 42f32        # Decimal number with type suffix
    /// ```
    e_num: struct {
        value: CIR.IntValue,
        kind: CIR.NumKind,
    },
    /// A 32-bit floating-point literal.
    e_frac_f32: struct {
        value: f32,
        has_suffix: bool, // If the value had a `f32` suffix
    },
    /// A 64-bit floating-point literal.
    /// Used for approximate decimal representations when F64 type is explicitly required for increased performance.
    ///
    /// ```roc
    /// 3.14f64     # Explicit F64 literal
    /// 2.5e10f64   # Scientific notation F64
    /// ```
    e_frac_f64: struct {
        value: f64,
        has_suffix: bool, // If the value had a `f64` suffix
    },
    /// A high-precision decimal literal.
    /// Used for exact decimal arithmetic without floating-point precision issues.
    /// Roc's preferred numeric type for most decimal calculations.
    ///
    /// ```roc
    /// 3.14159265358979323846    # High precision decimal
    /// 0.1 + 0.2                 # Equals exactly 0.3 (not 0.30000000000000004)
    /// ```
    e_dec: struct {
        value: RocDec,
        has_suffix: bool, // If the value had a `dec` suffix
    },
    /// A small decimal literal stored as a rational number (numerator/10^denominator).
    /// Memory-efficient representation for common decimal values.
    /// Avoids floating-point precision issues by using exact rational arithmetic.
    ///
    /// ```roc
    /// 3.14    # Stored as numerator=314, denominator_power_of_ten=2 (314/100)
    /// 0.5     # Stored as numerator=5, denominator_power_of_ten=1 (5/10)
    /// 42.0    # Stored as numerator=420, denominator_power_of_ten=1 (420/10)
    /// ```
    e_dec_small: struct {
        value: CIR.SmallDecValue,
        has_suffix: bool, // If the value had a `dec` suffix
    },
    // A single segment of a string literal
    // a single string may be made up of a span sequential segments
    // for example if it was split across multiple lines
    e_str_segment: struct {
        literal: StringLiteral.Idx,
    },
    // A string is combined of one or more segments, some of which may be interpolated
    // An interpolated string contains one or more non-string_segment's in the span
    e_str: struct {
        span: Expr.Span,
    },
    /// Lookup defined in this module
    /// ```roc
    /// foo = 42
    /// bar = foo + 1 # the "foo" here references the local "foo"
    /// ```
    e_lookup_local: struct {
        pattern_idx: CIR.Pattern.Idx,
    },
    /// Lookup defined in another module
    /// ```roc
    /// import json.Utf8
    /// foo = Utf8.encode("hello") # "Utf8.encode" is defined in another module
    /// ```
    e_lookup_external: struct {
        module_idx: CIR.Import.Idx,
        target_node_idx: u16,
        region: Region,
    },
    /// Lookup of a required identifier from the platform's `requires` clause.
    /// This represents a value that the app provides to the platform.
    /// ```roc
    /// platform "..."
    ///     requires {} { main! : () => {} }
    /// ...
    /// main_for_host! = main!  # "main!" here is a required lookup
    /// ```
    e_lookup_required: struct {
        /// Index into env.requires_types for this required identifier
        requires_idx: ModuleEnv.RequiredType.SafeList.Idx,
    },
    /// A sequence of zero or more elements of the same type
    /// ```roc
    /// ["one", "two", "three"]
    /// ```
    e_list: struct {
        elems: Expr.Span,
    },
    /// Empty list constant `[]`
    e_empty_list: struct {},
    /// Tuple expression zero or more elements of arbitrary type
    /// ```roc
    /// (1, "two", True)
    /// ```
    e_tuple: struct {
        elems: Expr.Span,
    },
    /// Match expression with one or more branches
    /// ```roc
    /// match x {
    ///     1 => "one",
    ///     a if a > 1 => "positive",
    ///     _ => "non-positive",
    /// }
    /// ```
    e_match: Match,
    /// If expression with one or more conditional branches and a final else clause.
    /// Roc's if expressions are expressions, not statements, so they always return a value.
    /// All branches must return the same type.
    ///
    /// ```roc
    /// if x >= 0 "positive" else "negative"
    /// ```
    e_if: struct {
        branches: IfBranch.Span,
        final_else: Expr.Idx,
    },
    /// This is *only* for calling functions, not for tag application.
    /// The Tag variant contains any applied values inside it.
    e_call: struct {
        func: Expr.Idx,
        args: Expr.Span,
        called_via: CalledVia,
    },
    /// Record literal with zero or more fields.
    /// Records are Roc's primary data structure for grouping related values.
    /// Field order doesn't matter for type compatibility.
    ///
    /// ```roc
    /// { name: "Alice", age: 30 }
    /// { x: 1.0, y: 2.0, z: 3.0 }
    /// { ..config, debug: True }  # Record update syntax
    /// ```
    e_record: struct {
        fields: CIR.RecordField.Span,
        ext: ?Expr.Idx,
    },
    /// Empty record constant
    e_empty_record: struct {},
    /// Block expression containing statements followed by a final expression.
    /// Blocks create a new scope and execute statements sequentially.
    /// The final expression determines the block's value and type.
    ///
    /// ```roc
    /// {
    ///     x = 42
    ///     y = x + 1
    ///     y * 2      # This expression is the block's value
    /// }
    /// ```
    e_block: struct {
        /// Statements executed in sequence
        stmts: CIR.Statement.Span,
        /// Final expression that produces the block's value
        final_expr: Expr.Idx,
    },
    /// Tag constructor with arguments (payload).
    /// Tags are used to create values of tag union types.
    /// Can have zero or more arguments of any type.
    ///
    /// ```roc
    /// Ok("success")          # Tag with string argument
    /// Circle(3.14)           # Tag with numeric argument
    /// Point(1.0, 2.0)        # Tag with multiple arguments
    /// Some([1, 2, 3])        # Tag with list argument
    /// ```
    e_tag: struct {
        name: Ident.Idx,
        args: Expr.Span,
    },
    /// A qualified, nominal type
    ///
    /// ```roc
    /// Try.Ok("success")       # Tags
    /// Config.{ optimize : Bool}  # Records
    /// Point.(1.0, 2.0)           # Tuples
    /// Point.(1.0)                # Values
    /// ```
    e_nominal: struct {
        nominal_type_decl: CIR.Statement.Idx,
        backing_expr: Expr.Idx,
        backing_type: NominalBackingType,
    },
    /// An external qualified, nominal type
    ///
    /// ```roc
    /// OtherModule.Try.Ok("success")       # Tags
    /// OtherModule.Config.{ optimize : Bool}  # Records
    /// OtherModule.Point.(1.0, 2.0)           # Tuples
    /// OtherModule.Point.(1.0)                # Values
    /// ```
    e_nominal_external: struct {
        module_idx: CIR.Import.Idx,
        target_node_idx: u16,
        backing_expr: Expr.Idx,
        backing_type: NominalBackingType,
    },
    /// Tag constructor with no arguments.
    /// Represents constant values in tag union types.
    /// Optimized representation for tags that carry no data.
    ///
    /// ```roc
    /// None
    /// Loading
    /// Red
    /// Empty
    /// ```
    e_zero_argument_tag: struct {
        closure_name: Ident.Idx,
        variant_var: TypeVar,
        ext_var: TypeVar,
        name: Ident.Idx,
    },
    /// A closure, which is a lambda expression that captures variables
    /// from its environment.
    e_closure: Closure,

    /// A pure lambda expression, with no captures. This represents the
    /// function's code before it's closed over.
    e_lambda: Lambda,
    /// Binary operation between two expressions.
    /// Includes arithmetic, comparison, logical, and pipe operators.
    ///
    /// ```roc
    /// 1 + 2              # Arithmetic: add
    /// x > y              # Comparison: greater than
    /// a and (b or c)     # Logical: and
    /// ```
    e_binop: Binop,
    /// Unary minus expression that negates a numeric value.
    ///
    /// ```roc
    /// -x              # Unary minus (numeric negation)
    /// -42             # Unary minus on literal
    /// ```
    e_unary_minus: UnaryMinus,
    /// Unary not expression that negates a boolean value.
    ///
    /// ```roc
    /// !x              # Unary not (boolean negation)
    /// !True           # Unary not on literal
    /// ```
    e_unary_not: UnaryNot,
    /// Dot access expression that represents field access or method calls.
    /// The exact meaning is determined after type inference based on the receiver's type:
    /// - Record field access: `person.name`
    /// - Static Dispatch (method-style) call: `list.map(fn)` (semantically this is equal to `List.map(list, fn)`)
    ///
    /// ```roc
    /// person.name         # Record field access
    /// list.len()          # Static Dispatch
    /// list.map(|x| x)     # Static Dispatch version of above
    /// ```
    e_dot_access: struct {
        receiver: Expr.Idx, // Expression before the dot (e.g., `list` in `list.map`)
        field_name: Ident.Idx, // Identifier after the dot (e.g., `map` in `list.map`)
        field_name_region: base.Region, // Region of just the field/method name for error reporting
        args: ?Expr.Span, // Optional arguments for method calls (e.g., `fn` in `list.map(fn)`)
    },
    /// Runtime error expression that crashes when executed.
    /// These are inserted during canonicalization when the compiler encounters
    /// semantic errors but continues compilation following the "inform don't block" philosophy.
    /// Common causes include undefined variables, type mismatches, and invalid operations.
    ///
    /// ```roc
    /// # This generates e_runtime_error for undefined variable 'x'
    /// y = x + 1
    ///
    /// # This generates e_runtime_error for type mismatch
    /// nums = [1, 2, "hello"]  # mixing numbers and strings
    /// ```
    e_runtime_error: struct {
        diagnostic: CIR.Diagnostic.Idx,
    },
    /// A crash expression that terminates execution with a message.
    /// This expression never returns and causes the program to crash at runtime.
    ///
    /// ```roc
    /// crash "Something went wrong"
    /// ```
    e_crash: struct {
        msg: StringLiteral.Idx,
    },
    /// A debug expression that prints the value of the inner expression.
    /// This expression evaluates to the same value as the inner expression
    /// but has the side effect of printing the value for debugging purposes.
    ///
    /// ```roc
    /// dbg someValue
    /// ```
    e_dbg: struct {
        expr: Expr.Idx,
    },
    /// An inspect expression that returns a string representation of a value.
    /// If the type has a `to_inspect : T -> Str` method, it calls that method.
    /// Otherwise, it generates an automatic string representation like the REPL.
    ///
    /// ```roc
    /// str = inspect someValue
    /// ```
    e_inspect: struct {
        expr: Expr.Idx,
    },
    /// An expect expression that performs a runtime assertion.
    /// This expression evaluates to empty record {} but can fail at runtime.
    /// Used for both top-level tests and inline assertions.
    ///
    /// ```roc
    /// expect [1,2,3].len() == 3
    /// ```
    e_expect: struct {
        body: Expr.Idx,
    },
    /// Ellipsis placeholder expression (...).
    /// This is valid syntax that represents an unimplemented expression.
    /// It will crash at runtime if execution reaches this point.
    ///
    /// ```roc
    /// launchTheNukes: |{}| ...
    /// ```
    e_ellipsis: struct {},
    /// A standalone type annotation without a body.
    /// This represents a type declaration that has no implementation.
    /// During type-checking, this expression is assigned the type from its annotation.
    ///
    /// ```roc
    /// foo : {} -> {}
    /// ```
    e_anno_only: struct {},

    /// Early return expression that exits the enclosing function with a value.
    /// This is used when `return` appears as the final expression in a block.
    /// Unlike a normal expression, evaluating this causes the function to return
    /// immediately with the contained value.
    ///
    /// ```roc
    /// if condition {
    ///     return value  # Early return from enclosing function
    /// }
    /// ```
    e_return: struct {
        expr: Expr.Idx,
    },

    /// For expression that iterates over a list and executes a body for each element.
    /// The for expression evaluates to the empty record `{}`.
    /// This is the expression form of a for loop, allowing it to be used in expression contexts.
    ///
    /// ```roc
    /// for_each! = |items, cb!| for item in items { cb!(item) }
    /// ```
    e_for: struct {
        patt: CIR.Pattern.Idx,
        expr: Expr.Idx,
        body: Expr.Idx,
    },

    /// A hosted function that will be provided by the platform at runtime.
    /// This represents a lambda/function whose implementation is provided by the host application
    /// via the RocOps.hosted_fns array.
    ///
    /// ```roc
    /// # Stdout.line! is a hosted function provided by the platform
    /// line! : Str => {}
    /// ```
    e_hosted_lambda: struct {
        symbol_name: base.Ident.Idx,
        index: u32, // Index into RocOps.hosted_fns (assigned during canonicalization)
        args: CIR.Pattern.Span,
        body: Expr.Idx,
    },

    /// A low-level builtin operation.
    /// This represents a lambda/function that will be implemented by the compiler backend.
    /// Like e_anno_only, it has no Roc implementation, but unlike e_anno_only,
    /// it's expected to be implemented by the backend rather than being an error.
    /// It behaves like e_lambda in that it has parameters and a body (which crashes when evaluated).
    ///
    /// ```roc
    /// # Str.is_empty is a low-level operation
    /// is_empty : Str -> Bool
    /// ```
    e_low_level_lambda: struct {
        op: LowLevel,
        args: CIR.Pattern.Span,
        body: Expr.Idx,
    },

    /// Low-level builtin operations that are implemented by the compiler backend.
    pub const LowLevel = enum {
        // String operations
        str_is_empty,
        str_is_eq,
        str_concat,
        str_contains,
        str_trim,
        str_trim_start,
        str_trim_end,
        str_caseless_ascii_equals,
        str_with_ascii_lowercased,
        str_with_ascii_uppercased,
        str_starts_with,
        str_ends_with,
        str_repeat,
        str_with_prefix,
        str_drop_prefix,
        str_drop_suffix,
        str_count_utf8_bytes,
        str_with_capacity,
        str_reserve,
        str_release_excess_capacity,
        str_to_utf8,
        str_from_utf8_lossy,
        str_from_utf8,
        str_split_on,
        str_join_with,

        // Numeric to_str operations
        u8_to_str,
        i8_to_str,
        u16_to_str,
        i16_to_str,
        u32_to_str,
        i32_to_str,
        u64_to_str,
        i64_to_str,
        u128_to_str,
        i128_to_str,
        dec_to_str,
        f32_to_str,
        f64_to_str,

        // List operations
        list_len,
        list_is_empty,
        list_get_unsafe,
        list_concat,
        list_with_capacity,
        list_sort_with,
        list_drop_at,
        list_sublist,
        list_append,

        // Set operations
        // set_is_empty,

        // Bool operations
        bool_is_eq,

        // Numeric type checking operations
        num_is_zero, // All numeric types
        num_is_negative, // Signed types only: I8, I16, I32, I64, I128, Dec, F32, F64
        num_is_positive, // Signed types only: I8, I16, I32, I64, I128, Dec, F32, F64

        // Numeric comparison operations
        num_is_eq, // All integer types + Dec (NOT F32/F64 due to float imprecision)
        num_is_gt, // All numeric types
        num_is_gte, // All numeric types
        num_is_lt, // All numeric types
        num_is_lte, // All numeric types

        // Numeric arithmetic operations
        num_negate, // Signed types only: I8, I16, I32, I64, I128, Dec, F32, F64
        num_abs, // Signed types only: I8, I16, I32, I64, I128, Dec, F32, F64
        num_abs_diff, // All numeric types (signed returns unsigned counterpart)
        num_plus, // All numeric types
        num_minus, // All numeric types
        num_times, // All numeric types
        num_div_by, // All numeric types
        num_div_trunc_by, // All numeric types
        num_rem_by, // All numeric types
        num_mod_by, // Integer types only: U8, I8, U16, I16, U32, I32, U64, I64, U128, I128

        // Numeric parsing operations
        num_from_int_digits, // Parse List(U8) -> Try(num, [OutOfRange])
        num_from_dec_digits, // Parse (List(U8), List(U8)) -> Try(num, [OutOfRange])
        num_from_numeral, // Parse Numeral -> Try(num, [InvalidNumeral(Str)])
        num_from_str, // Parse Str -> Try(num, [BadNumStr])

        // Numeric conversion operations (U8)
        u8_to_i8_wrap, // U8 -> I8 (wrapping)
        u8_to_i8_try, // U8 -> Try(I8, [OutOfRange])
        u8_to_i16, // U8 -> I16 (safe)
        u8_to_i32, // U8 -> I32 (safe)
        u8_to_i64, // U8 -> I64 (safe)
        u8_to_i128, // U8 -> I128 (safe)
        u8_to_u16, // U8 -> U16 (safe)
        u8_to_u32, // U8 -> U32 (safe)
        u8_to_u64, // U8 -> U64 (safe)
        u8_to_u128, // U8 -> U128 (safe)
        u8_to_f32, // U8 -> F32 (safe)
        u8_to_f64, // U8 -> F64 (safe)
        u8_to_dec, // U8 -> Dec (safe)

        // Numeric conversion operations (I8)
        i8_to_i16, // I8 -> I16 (safe)
        i8_to_i32, // I8 -> I32 (safe)
        i8_to_i64, // I8 -> I64 (safe)
        i8_to_i128, // I8 -> I128 (safe)
        i8_to_u8_wrap, // I8 -> U8 (wrapping)
        i8_to_u8_try, // I8 -> Try(U8, [OutOfRange])
        i8_to_u16_wrap, // I8 -> U16 (wrapping)
        i8_to_u16_try, // I8 -> Try(U16, [OutOfRange])
        i8_to_u32_wrap, // I8 -> U32 (wrapping)
        i8_to_u32_try, // I8 -> Try(U32, [OutOfRange])
        i8_to_u64_wrap, // I8 -> U64 (wrapping)
        i8_to_u64_try, // I8 -> Try(U64, [OutOfRange])
        i8_to_u128_wrap, // I8 -> U128 (wrapping)
        i8_to_u128_try, // I8 -> Try(U128, [OutOfRange])
        i8_to_f32, // I8 -> F32 (safe)
        i8_to_f64, // I8 -> F64 (safe)
        i8_to_dec, // I8 -> Dec (safe)

        // Numeric conversion operations (U16)
        u16_to_i8_wrap, // U16 -> I8 (wrapping)
        u16_to_i8_try, // U16 -> Try(I8, [OutOfRange])
        u16_to_i16_wrap, // U16 -> I16 (wrapping)
        u16_to_i16_try, // U16 -> Try(I16, [OutOfRange])
        u16_to_i32, // U16 -> I32 (safe)
        u16_to_i64, // U16 -> I64 (safe)
        u16_to_i128, // U16 -> I128 (safe)
        u16_to_u8_wrap, // U16 -> U8 (wrapping)
        u16_to_u8_try, // U16 -> Try(U8, [OutOfRange])
        u16_to_u32, // U16 -> U32 (safe)
        u16_to_u64, // U16 -> U64 (safe)
        u16_to_u128, // U16 -> U128 (safe)
        u16_to_f32, // U16 -> F32 (safe)
        u16_to_f64, // U16 -> F64 (safe)
        u16_to_dec, // U16 -> Dec (safe)

        // Numeric conversion operations (I16)
        i16_to_i8_wrap, // I16 -> I8 (wrapping)
        i16_to_i8_try, // I16 -> Try(I8, [OutOfRange])
        i16_to_i32, // I16 -> I32 (safe)
        i16_to_i64, // I16 -> I64 (safe)
        i16_to_i128, // I16 -> I128 (safe)
        i16_to_u8_wrap, // I16 -> U8 (wrapping)
        i16_to_u8_try, // I16 -> Try(U8, [OutOfRange])
        i16_to_u16_wrap, // I16 -> U16 (wrapping)
        i16_to_u16_try, // I16 -> Try(U16, [OutOfRange])
        i16_to_u32_wrap, // I16 -> U32 (wrapping)
        i16_to_u32_try, // I16 -> Try(U32, [OutOfRange])
        i16_to_u64_wrap, // I16 -> U64 (wrapping)
        i16_to_u64_try, // I16 -> Try(U64, [OutOfRange])
        i16_to_u128_wrap, // I16 -> U128 (wrapping)
        i16_to_u128_try, // I16 -> Try(U128, [OutOfRange])
        i16_to_f32, // I16 -> F32 (safe)
        i16_to_f64, // I16 -> F64 (safe)
        i16_to_dec, // I16 -> Dec (safe)

        // Numeric conversion operations (U32)
        u32_to_i8_wrap, // U32 -> I8 (wrapping)
        u32_to_i8_try, // U32 -> Try(I8, [OutOfRange])
        u32_to_i16_wrap, // U32 -> I16 (wrapping)
        u32_to_i16_try, // U32 -> Try(I16, [OutOfRange])
        u32_to_i32_wrap, // U32 -> I32 (wrapping)
        u32_to_i32_try, // U32 -> Try(I32, [OutOfRange])
        u32_to_i64, // U32 -> I64 (safe)
        u32_to_i128, // U32 -> I128 (safe)
        u32_to_u8_wrap, // U32 -> U8 (wrapping)
        u32_to_u8_try, // U32 -> Try(U8, [OutOfRange])
        u32_to_u16_wrap, // U32 -> U16 (wrapping)
        u32_to_u16_try, // U32 -> Try(U16, [OutOfRange])
        u32_to_u64, // U32 -> U64 (safe)
        u32_to_u128, // U32 -> U128 (safe)
        u32_to_f32, // U32 -> F32 (safe)
        u32_to_f64, // U32 -> F64 (safe)
        u32_to_dec, // U32 -> Dec (safe)

        // Numeric conversion operations (I32)
        i32_to_i8_wrap, // I32 -> I8 (wrapping)
        i32_to_i8_try, // I32 -> Try(I8, [OutOfRange])
        i32_to_i16_wrap, // I32 -> I16 (wrapping)
        i32_to_i16_try, // I32 -> Try(I16, [OutOfRange])
        i32_to_i64, // I32 -> I64 (safe)
        i32_to_i128, // I32 -> I128 (safe)
        i32_to_u8_wrap, // I32 -> U8 (wrapping)
        i32_to_u8_try, // I32 -> Try(U8, [OutOfRange])
        i32_to_u16_wrap, // I32 -> U16 (wrapping)
        i32_to_u16_try, // I32 -> Try(U16, [OutOfRange])
        i32_to_u32_wrap, // I32 -> U32 (wrapping)
        i32_to_u32_try, // I32 -> Try(U32, [OutOfRange])
        i32_to_u64_wrap, // I32 -> U64 (wrapping)
        i32_to_u64_try, // I32 -> Try(U64, [OutOfRange])
        i32_to_u128_wrap, // I32 -> U128 (wrapping)
        i32_to_u128_try, // I32 -> Try(U128, [OutOfRange])
        i32_to_f32, // I32 -> F32 (safe)
        i32_to_f64, // I32 -> F64 (safe)
        i32_to_dec, // I32 -> Dec (safe)

        // Numeric conversion operations (U64)
        u64_to_i8_wrap, // U64 -> I8 (wrapping)
        u64_to_i8_try, // U64 -> Try(I8, [OutOfRange])
        u64_to_i16_wrap, // U64 -> I16 (wrapping)
        u64_to_i16_try, // U64 -> Try(I16, [OutOfRange])
        u64_to_i32_wrap, // U64 -> I32 (wrapping)
        u64_to_i32_try, // U64 -> Try(I32, [OutOfRange])
        u64_to_i64_wrap, // U64 -> I64 (wrapping)
        u64_to_i64_try, // U64 -> Try(I64, [OutOfRange])
        u64_to_i128, // U64 -> I128 (safe)
        u64_to_u8_wrap, // U64 -> U8 (wrapping)
        u64_to_u8_try, // U64 -> Try(U8, [OutOfRange])
        u64_to_u16_wrap, // U64 -> U16 (wrapping)
        u64_to_u16_try, // U64 -> Try(U16, [OutOfRange])
        u64_to_u32_wrap, // U64 -> U32 (wrapping)
        u64_to_u32_try, // U64 -> Try(U32, [OutOfRange])
        u64_to_u128, // U64 -> U128 (safe)
        u64_to_f32, // U64 -> F32 (safe)
        u64_to_f64, // U64 -> F64 (safe)
        u64_to_dec, // U64 -> Dec (safe)

        // Numeric conversion operations (I64)
        i64_to_i8_wrap, // I64 -> I8 (wrapping)
        i64_to_i8_try, // I64 -> Try(I8, [OutOfRange])
        i64_to_i16_wrap, // I64 -> I16 (wrapping)
        i64_to_i16_try, // I64 -> Try(I16, [OutOfRange])
        i64_to_i32_wrap, // I64 -> I32 (wrapping)
        i64_to_i32_try, // I64 -> Try(I32, [OutOfRange])
        i64_to_i128, // I64 -> I128 (safe)
        i64_to_u8_wrap, // I64 -> U8 (wrapping)
        i64_to_u8_try, // I64 -> Try(U8, [OutOfRange])
        i64_to_u16_wrap, // I64 -> U16 (wrapping)
        i64_to_u16_try, // I64 -> Try(U16, [OutOfRange])
        i64_to_u32_wrap, // I64 -> U32 (wrapping)
        i64_to_u32_try, // I64 -> Try(U32, [OutOfRange])
        i64_to_u64_wrap, // I64 -> U64 (wrapping)
        i64_to_u64_try, // I64 -> Try(U64, [OutOfRange])
        i64_to_u128_wrap, // I64 -> U128 (wrapping)
        i64_to_u128_try, // I64 -> Try(U128, [OutOfRange])
        i64_to_f32, // I64 -> F32 (safe)
        i64_to_f64, // I64 -> F64 (safe)
        i64_to_dec, // I64 -> Dec (safe)

        // Numeric conversion operations (U128)
        u128_to_i8_wrap, // U128 -> I8 (wrapping)
        u128_to_i8_try, // U128 -> Try(I8, [OutOfRange])
        u128_to_i16_wrap, // U128 -> I16 (wrapping)
        u128_to_i16_try, // U128 -> Try(I16, [OutOfRange])
        u128_to_i32_wrap, // U128 -> I32 (wrapping)
        u128_to_i32_try, // U128 -> Try(I32, [OutOfRange])
        u128_to_i64_wrap, // U128 -> I64 (wrapping)
        u128_to_i64_try, // U128 -> Try(I64, [OutOfRange])
        u128_to_i128_wrap, // U128 -> I128 (wrapping)
        u128_to_i128_try, // U128 -> Try(I128, [OutOfRange])
        u128_to_u8_wrap, // U128 -> U8 (wrapping)
        u128_to_u8_try, // U128 -> Try(U8, [OutOfRange])
        u128_to_u16_wrap, // U128 -> U16 (wrapping)
        u128_to_u16_try, // U128 -> Try(U16, [OutOfRange])
        u128_to_u32_wrap, // U128 -> U32 (wrapping)
        u128_to_u32_try, // U128 -> Try(U32, [OutOfRange])
        u128_to_u64_wrap, // U128 -> U64 (wrapping)
        u128_to_u64_try, // U128 -> Try(U64, [OutOfRange])
        u128_to_f32, // U128 -> F32 (safe)
        u128_to_f64, // U128 -> F64 (safe)
        u128_to_dec_try_unsafe, // U128 -> { success: Bool, val: Dec }

        // Numeric conversion operations (I128)
        i128_to_i8_wrap, // I128 -> I8 (wrapping)
        i128_to_i8_try, // I128 -> Try(I8, [OutOfRange])
        i128_to_i16_wrap, // I128 -> I16 (wrapping)
        i128_to_i16_try, // I128 -> Try(I16, [OutOfRange])
        i128_to_i32_wrap, // I128 -> I32 (wrapping)
        i128_to_i32_try, // I128 -> Try(I32, [OutOfRange])
        i128_to_i64_wrap, // I128 -> I64 (wrapping)
        i128_to_i64_try, // I128 -> Try(I64, [OutOfRange])
        i128_to_u8_wrap, // I128 -> U8 (wrapping)
        i128_to_u8_try, // I128 -> Try(U8, [OutOfRange])
        i128_to_u16_wrap, // I128 -> U16 (wrapping)
        i128_to_u16_try, // I128 -> Try(U16, [OutOfRange])
        i128_to_u32_wrap, // I128 -> U32 (wrapping)
        i128_to_u32_try, // I128 -> Try(U32, [OutOfRange])
        i128_to_u64_wrap, // I128 -> U64 (wrapping)
        i128_to_u64_try, // I128 -> Try(U64, [OutOfRange])
        i128_to_u128_wrap, // I128 -> U128 (wrapping)
        i128_to_u128_try, // I128 -> Try(U128, [OutOfRange])
        i128_to_f32, // I128 -> F32 (safe)
        i128_to_f64, // I128 -> F64 (safe)
        i128_to_dec_try_unsafe, // I128 -> { success: Bool, val: Dec }

        // Numeric conversion operations (F32)
        f32_to_i8_trunc, // F32 -> I8 (truncating)
        f32_to_i8_try_unsafe, // F32 -> { is_int: Bool, in_range: Bool, val: I8 }
        f32_to_i16_trunc, // F32 -> I16 (truncating)
        f32_to_i16_try_unsafe, // F32 -> { is_int: Bool, in_range: Bool, val: I16 }
        f32_to_i32_trunc, // F32 -> I32 (truncating)
        f32_to_i32_try_unsafe, // F32 -> { is_int: Bool, in_range: Bool, val: I32 }
        f32_to_i64_trunc, // F32 -> I64 (truncating)
        f32_to_i64_try_unsafe, // F32 -> { is_int: Bool, in_range: Bool, val: I64 }
        f32_to_i128_trunc, // F32 -> I128 (truncating)
        f32_to_i128_try_unsafe, // F32 -> { is_int: Bool, in_range: Bool, val: I128 }
        f32_to_u8_trunc, // F32 -> U8 (truncating)
        f32_to_u8_try_unsafe, // F32 -> { is_int: Bool, in_range: Bool, val: U8 }
        f32_to_u16_trunc, // F32 -> U16 (truncating)
        f32_to_u16_try_unsafe, // F32 -> { is_int: Bool, in_range: Bool, val: U16 }
        f32_to_u32_trunc, // F32 -> U32 (truncating)
        f32_to_u32_try_unsafe, // F32 -> { is_int: Bool, in_range: Bool, val: U32 }
        f32_to_u64_trunc, // F32 -> U64 (truncating)
        f32_to_u64_try_unsafe, // F32 -> { is_int: Bool, in_range: Bool, val: U64 }
        f32_to_u128_trunc, // F32 -> U128 (truncating)
        f32_to_u128_try_unsafe, // F32 -> { is_int: Bool, in_range: Bool, val: U128 }
        f32_to_f64, // F32 -> F64 (safe widening)

        // Numeric conversion operations (F64)
        f64_to_i8_trunc, // F64 -> I8 (truncating)
        f64_to_i8_try_unsafe, // F64 -> { is_int: Bool, in_range: Bool, val: I8 }
        f64_to_i16_trunc, // F64 -> I16 (truncating)
        f64_to_i16_try_unsafe, // F64 -> { is_int: Bool, in_range: Bool, val: I16 }
        f64_to_i32_trunc, // F64 -> I32 (truncating)
        f64_to_i32_try_unsafe, // F64 -> { is_int: Bool, in_range: Bool, val: I32 }
        f64_to_i64_trunc, // F64 -> I64 (truncating)
        f64_to_i64_try_unsafe, // F64 -> { is_int: Bool, in_range: Bool, val: I64 }
        f64_to_i128_trunc, // F64 -> I128 (truncating)
        f64_to_i128_try_unsafe, // F64 -> { is_int: Bool, in_range: Bool, val: I128 }
        f64_to_u8_trunc, // F64 -> U8 (truncating)
        f64_to_u8_try_unsafe, // F64 -> { is_int: Bool, in_range: Bool, val: U8 }
        f64_to_u16_trunc, // F64 -> U16 (truncating)
        f64_to_u16_try_unsafe, // F64 -> { is_int: Bool, in_range: Bool, val: U16 }
        f64_to_u32_trunc, // F64 -> U32 (truncating)
        f64_to_u32_try_unsafe, // F64 -> { is_int: Bool, in_range: Bool, val: U32 }
        f64_to_u64_trunc, // F64 -> U64 (truncating)
        f64_to_u64_try_unsafe, // F64 -> { is_int: Bool, in_range: Bool, val: U64 }
        f64_to_u128_trunc, // F64 -> U128 (truncating)
        f64_to_u128_try_unsafe, // F64 -> { is_int: Bool, in_range: Bool, val: U128 }
        f64_to_f32_wrap, // F64 -> F32 (lossy narrowing)
        f64_to_f32_try_unsafe, // F64 -> { success: Bool, val: F32 }

        // Numeric conversion operations (Dec)
        dec_to_i8_trunc, // Dec -> I8 (truncating)
        dec_to_i8_try_unsafe, // Dec -> { is_int: Bool, in_range: Bool, val: I8 }
        dec_to_i16_trunc, // Dec -> I16 (truncating)
        dec_to_i16_try_unsafe, // Dec -> { is_int: Bool, in_range: Bool, val: I16 }
        dec_to_i32_trunc, // Dec -> I32 (truncating)
        dec_to_i32_try_unsafe, // Dec -> { is_int: Bool, in_range: Bool, val: I32 }
        dec_to_i64_trunc, // Dec -> I64 (truncating)
        dec_to_i64_try_unsafe, // Dec -> { is_int: Bool, in_range: Bool, val: I64 }
        dec_to_i128_trunc, // Dec -> I128 (truncating)
        dec_to_i128_try_unsafe, // Dec -> { is_int: Bool, val: I128 } - always in range
        dec_to_u8_trunc, // Dec -> U8 (truncating)
        dec_to_u8_try_unsafe, // Dec -> { is_int: Bool, in_range: Bool, val: U8 }
        dec_to_u16_trunc, // Dec -> U16 (truncating)
        dec_to_u16_try_unsafe, // Dec -> { is_int: Bool, in_range: Bool, val: U16 }
        dec_to_u32_trunc, // Dec -> U32 (truncating)
        dec_to_u32_try_unsafe, // Dec -> { is_int: Bool, in_range: Bool, val: U32 }
        dec_to_u64_trunc, // Dec -> U64 (truncating)
        dec_to_u64_try_unsafe, // Dec -> { is_int: Bool, in_range: Bool, val: U64 }
        dec_to_u128_trunc, // Dec -> U128 (truncating)
        dec_to_u128_try_unsafe, // Dec -> { is_int: Bool, in_range: Bool, val: U128 }
        dec_to_f32_wrap, // Dec -> F32 (lossy narrowing)
        dec_to_f32_try_unsafe, // Dec -> { success: Bool, val: F32 }
        dec_to_f64, // Dec -> F64 (lossy conversion)

        /// Ownership semantics for each argument of a low-level operation.
        /// See src/builtins/OWNERSHIP.md for detailed documentation.
        pub const ArgOwnership = enum {
            /// Function reads argument without affecting refcount. Caller retains ownership.
            /// Interpreter should decref after call.
            borrow,
            /// Function takes ownership of argument. Caller loses access.
            /// Interpreter should NOT decref after call.
            consume,
        };

        /// Returns the ownership semantics for each argument of this low-level operation.
        /// The returned slice has one entry per argument.
        ///
        /// Important: DO NOT ADD an else branch to this switch statement
        /// we expect a compile error if a new case is added and ownership semantics are not defined here.
        pub fn getArgOwnership(self: LowLevel) []const ArgOwnership {
            return switch (self) {
                // String operations - borrowing (read-only)
                .str_is_empty, .str_is_eq, .str_contains, .str_starts_with, .str_ends_with, .str_count_utf8_bytes, .str_caseless_ascii_equals => &.{ .borrow, .borrow },

                // String operations - consuming (take ownership)
                .str_concat => &.{ .consume, .borrow }, // first consumed, second borrowed
                .str_trim, .str_trim_start, .str_trim_end => &.{.consume},
                .str_with_ascii_lowercased, .str_with_ascii_uppercased => &.{.consume},
                .str_repeat => &.{ .borrow, .borrow }, // string borrowed, count is value type
                .str_with_prefix => &.{ .consume, .borrow },
                .str_with_capacity => &.{.borrow}, // capacity is value type
                .str_reserve => &.{ .consume, .borrow },
                .str_release_excess_capacity => &.{.consume},
                .str_join_with => &.{ .consume, .borrow }, // list consumed, separator borrowed

                // String operations - borrowing with seamless slice result (incref internally)
                .str_split_on => &.{ .borrow, .borrow },
                .str_to_utf8 => &.{.borrow},
                .str_drop_prefix, .str_drop_suffix => &.{ .borrow, .borrow },

                // String parsing - list consumed
                .str_from_utf8, .str_from_utf8_lossy => &.{.consume},

                // Numeric to_str - value types (no ownership)
                .u8_to_str, .i8_to_str, .u16_to_str, .i16_to_str, .u32_to_str, .i32_to_str, .u64_to_str, .i64_to_str, .u128_to_str, .i128_to_str, .dec_to_str, .f32_to_str, .f64_to_str => &.{.borrow},

                // List operations - borrowing
                .list_len, .list_is_empty, .list_get_unsafe => &.{.borrow},

                // List operations - consuming
                .list_concat => &.{ .consume, .consume },
                .list_with_capacity => &.{.borrow}, // capacity is value type
                .list_sort_with => &.{.consume},
                .list_append => &.{ .consume, .borrow }, // list consumed, element borrowed
                .list_drop_at => &.{ .consume, .borrow }, // list consumed, index is value type
                .list_sublist => &.{ .consume, .borrow }, // list consumed, {start, len} record is value type

                // Bool operations - value types
                .bool_is_eq => &.{ .borrow, .borrow },

                // Numeric operations - all value types (no heap allocation)
                .num_is_zero, .num_is_negative, .num_is_positive, .num_negate, .num_abs => &.{.borrow},
                .num_is_eq, .num_is_gt, .num_is_gte, .num_is_lt, .num_is_lte, .num_plus, .num_minus, .num_times, .num_div_by, .num_div_trunc_by, .num_rem_by, .num_mod_by, .num_abs_diff => &.{ .borrow, .borrow },

                // Numeric parsing - list borrowed for digits, string borrowed
                .num_from_int_digits => &.{.borrow},
                .num_from_dec_digits => &.{ .borrow, .borrow },
                .num_from_numeral => &.{.borrow},
                .num_from_str => &.{.borrow},

                // All numeric conversions are value types (no heap allocation).
                // Explicitly listed to get compile errors when new LowLevel variants are added.
                .u8_to_i8_wrap,
                .u8_to_i8_try,
                .u8_to_i16,
                .u8_to_i32,
                .u8_to_i64,
                .u8_to_i128,
                .u8_to_u16,
                .u8_to_u32,
                .u8_to_u64,
                .u8_to_u128,
                .u8_to_f32,
                .u8_to_f64,
                .u8_to_dec,
                .i8_to_i16,
                .i8_to_i32,
                .i8_to_i64,
                .i8_to_i128,
                .i8_to_u8_wrap,
                .i8_to_u8_try,
                .i8_to_u16_wrap,
                .i8_to_u16_try,
                .i8_to_u32_wrap,
                .i8_to_u32_try,
                .i8_to_u64_wrap,
                .i8_to_u64_try,
                .i8_to_u128_wrap,
                .i8_to_u128_try,
                .i8_to_f32,
                .i8_to_f64,
                .i8_to_dec,
                .u16_to_i8_wrap,
                .u16_to_i8_try,
                .u16_to_i16_wrap,
                .u16_to_i16_try,
                .u16_to_i32,
                .u16_to_i64,
                .u16_to_i128,
                .u16_to_u8_wrap,
                .u16_to_u8_try,
                .u16_to_u32,
                .u16_to_u64,
                .u16_to_u128,
                .u16_to_f32,
                .u16_to_f64,
                .u16_to_dec,
                .i16_to_i8_wrap,
                .i16_to_i8_try,
                .i16_to_i32,
                .i16_to_i64,
                .i16_to_i128,
                .i16_to_u8_wrap,
                .i16_to_u8_try,
                .i16_to_u16_wrap,
                .i16_to_u16_try,
                .i16_to_u32_wrap,
                .i16_to_u32_try,
                .i16_to_u64_wrap,
                .i16_to_u64_try,
                .i16_to_u128_wrap,
                .i16_to_u128_try,
                .i16_to_f32,
                .i16_to_f64,
                .i16_to_dec,
                .u32_to_i8_wrap,
                .u32_to_i8_try,
                .u32_to_i16_wrap,
                .u32_to_i16_try,
                .u32_to_i32_wrap,
                .u32_to_i32_try,
                .u32_to_i64,
                .u32_to_i128,
                .u32_to_u8_wrap,
                .u32_to_u8_try,
                .u32_to_u16_wrap,
                .u32_to_u16_try,
                .u32_to_u64,
                .u32_to_u128,
                .u32_to_f32,
                .u32_to_f64,
                .u32_to_dec,
                .i32_to_i8_wrap,
                .i32_to_i8_try,
                .i32_to_i16_wrap,
                .i32_to_i16_try,
                .i32_to_i64,
                .i32_to_i128,
                .i32_to_u8_wrap,
                .i32_to_u8_try,
                .i32_to_u16_wrap,
                .i32_to_u16_try,
                .i32_to_u32_wrap,
                .i32_to_u32_try,
                .i32_to_u64_wrap,
                .i32_to_u64_try,
                .i32_to_u128_wrap,
                .i32_to_u128_try,
                .i32_to_f32,
                .i32_to_f64,
                .i32_to_dec,
                .u64_to_i8_wrap,
                .u64_to_i8_try,
                .u64_to_i16_wrap,
                .u64_to_i16_try,
                .u64_to_i32_wrap,
                .u64_to_i32_try,
                .u64_to_i64_wrap,
                .u64_to_i64_try,
                .u64_to_i128,
                .u64_to_u8_wrap,
                .u64_to_u8_try,
                .u64_to_u16_wrap,
                .u64_to_u16_try,
                .u64_to_u32_wrap,
                .u64_to_u32_try,
                .u64_to_u128,
                .u64_to_f32,
                .u64_to_f64,
                .u64_to_dec,
                .i64_to_i8_wrap,
                .i64_to_i8_try,
                .i64_to_i16_wrap,
                .i64_to_i16_try,
                .i64_to_i32_wrap,
                .i64_to_i32_try,
                .i64_to_i128,
                .i64_to_u8_wrap,
                .i64_to_u8_try,
                .i64_to_u16_wrap,
                .i64_to_u16_try,
                .i64_to_u32_wrap,
                .i64_to_u32_try,
                .i64_to_u64_wrap,
                .i64_to_u64_try,
                .i64_to_u128_wrap,
                .i64_to_u128_try,
                .i64_to_f32,
                .i64_to_f64,
                .i64_to_dec,
                .u128_to_i8_wrap,
                .u128_to_i8_try,
                .u128_to_i16_wrap,
                .u128_to_i16_try,
                .u128_to_i32_wrap,
                .u128_to_i32_try,
                .u128_to_i64_wrap,
                .u128_to_i64_try,
                .u128_to_i128_wrap,
                .u128_to_i128_try,
                .u128_to_u8_wrap,
                .u128_to_u8_try,
                .u128_to_u16_wrap,
                .u128_to_u16_try,
                .u128_to_u32_wrap,
                .u128_to_u32_try,
                .u128_to_u64_wrap,
                .u128_to_u64_try,
                .u128_to_f32,
                .u128_to_f64,
                .u128_to_dec_try_unsafe,
                .i128_to_i8_wrap,
                .i128_to_i8_try,
                .i128_to_i16_wrap,
                .i128_to_i16_try,
                .i128_to_i32_wrap,
                .i128_to_i32_try,
                .i128_to_i64_wrap,
                .i128_to_i64_try,
                .i128_to_u8_wrap,
                .i128_to_u8_try,
                .i128_to_u16_wrap,
                .i128_to_u16_try,
                .i128_to_u32_wrap,
                .i128_to_u32_try,
                .i128_to_u64_wrap,
                .i128_to_u64_try,
                .i128_to_u128_wrap,
                .i128_to_u128_try,
                .i128_to_f32,
                .i128_to_f64,
                .i128_to_dec_try_unsafe,
                .f32_to_i8_trunc,
                .f32_to_i8_try_unsafe,
                .f32_to_i16_trunc,
                .f32_to_i16_try_unsafe,
                .f32_to_i32_trunc,
                .f32_to_i32_try_unsafe,
                .f32_to_i64_trunc,
                .f32_to_i64_try_unsafe,
                .f32_to_i128_trunc,
                .f32_to_i128_try_unsafe,
                .f32_to_u8_trunc,
                .f32_to_u8_try_unsafe,
                .f32_to_u16_trunc,
                .f32_to_u16_try_unsafe,
                .f32_to_u32_trunc,
                .f32_to_u32_try_unsafe,
                .f32_to_u64_trunc,
                .f32_to_u64_try_unsafe,
                .f32_to_u128_trunc,
                .f32_to_u128_try_unsafe,
                .f32_to_f64,
                .f64_to_i8_trunc,
                .f64_to_i8_try_unsafe,
                .f64_to_i16_trunc,
                .f64_to_i16_try_unsafe,
                .f64_to_i32_trunc,
                .f64_to_i32_try_unsafe,
                .f64_to_i64_trunc,
                .f64_to_i64_try_unsafe,
                .f64_to_i128_trunc,
                .f64_to_i128_try_unsafe,
                .f64_to_u8_trunc,
                .f64_to_u8_try_unsafe,
                .f64_to_u16_trunc,
                .f64_to_u16_try_unsafe,
                .f64_to_u32_trunc,
                .f64_to_u32_try_unsafe,
                .f64_to_u64_trunc,
                .f64_to_u64_try_unsafe,
                .f64_to_u128_trunc,
                .f64_to_u128_try_unsafe,
                .f64_to_f32_wrap,
                .f64_to_f32_try_unsafe,
                .dec_to_i8_trunc,
                .dec_to_i8_try_unsafe,
                .dec_to_i16_trunc,
                .dec_to_i16_try_unsafe,
                .dec_to_i32_trunc,
                .dec_to_i32_try_unsafe,
                .dec_to_i64_trunc,
                .dec_to_i64_try_unsafe,
                .dec_to_i128_trunc,
                .dec_to_i128_try_unsafe,
                .dec_to_u8_trunc,
                .dec_to_u8_try_unsafe,
                .dec_to_u16_trunc,
                .dec_to_u16_try_unsafe,
                .dec_to_u32_trunc,
                .dec_to_u32_try_unsafe,
                .dec_to_u64_trunc,
                .dec_to_u64_try_unsafe,
                .dec_to_u128_trunc,
                .dec_to_u128_try_unsafe,
                .dec_to_f32_wrap,
                .dec_to_f32_try_unsafe,
                .dec_to_f64,
                => &.{.borrow},
            };
        }
    };

    pub const Idx = enum(u32) { _ };
    pub const Span = extern struct { span: DataSpan };

    /// A single branch of an if expression.
    /// Contains a condition expression and the body to execute if the condition is true.
    ///
    /// ```roc
    /// if x >= 0 {         # condition: x >= 0
    ///     "positive"      # body: "positive"
    /// } else if x < 0 {   # condition: x < 0
    ///     "negative"      # body: "negative"
    /// }
    /// ```
    pub const IfBranch = struct {
        cond: Expr.Idx,
        body: Expr.Idx,

        pub const Idx = enum(u32) { _ };
        pub const Span = extern struct { span: base.DataSpan };
    };

    /// A closure, which is a lambda expression that captures variables
    /// from its environment.
    pub const Closure = struct {
        lambda_idx: Expr.Idx, // An index pointing to an `e_lambda` expression
        captures: Expr.Capture.Span,
    };

    /// A pure lambda expression, with no captures. This represents the
    /// function's code before it's closed over.
    pub const Lambda = struct {
        args: CIR.Pattern.Span,
        body: Expr.Idx,
    };

    pub fn initStr(expr_span: Expr.Span) Expr {
        return Self{
            .e_str = .{
                .span = expr_span,
            },
        };
    }

    pub fn initStrSegment(literal: StringLiteral.Idx) Expr {
        return Self{
            .e_str_segment = .{
                .literal = literal,
            },
        };
    }

    /// A binary operation between two expressions.
    /// Represents infix operators that take two operands.
    ///
    /// ```roc
    /// 1 + 2       # add
    /// x > y       # gt (greater than)
    /// a and b     # and (logical AND)
    /// xs |> f     # pipe operator (not valid syntax, used to provide nice error messages)
    /// ```
    pub const Binop = struct {
        op: Op,
        lhs: Expr.Idx,
        rhs: Expr.Idx,

        /// Binary operators available in Roc.
        pub const Op = enum {
            add, // +
            sub, // -
            mul, // *
            div, // /
            rem, // %
            lt, // <
            gt, // >
            le, // <=
            ge, // >=
            eq, // ==
            ne, // !=
            div_trunc, // //
            @"and", // and
            @"or", // or
        };

        pub fn init(op: Op, lhs: Expr.Idx, rhs: Expr.Idx) Binop {
            return Binop{ .op = op, .lhs = lhs, .rhs = rhs };
        }
    };

    /// Unary minus operation for numeric negation.
    pub const UnaryMinus = struct {
        expr: Expr.Idx,

        pub fn init(expr: Expr.Idx) UnaryMinus {
            return UnaryMinus{ .expr = expr };
        }
    };

    /// Unary not operation for boolean negation.
    pub const UnaryNot = struct {
        expr: Expr.Idx,

        pub fn init(expr: Expr.Idx) UnaryNot {
            return UnaryNot{ .expr = expr };
        }
    };

    /// The type inside a nominal var
    pub const NominalBackingType = enum { tag, record, tuple, value };

    pub fn pushToSExprTree(self: *const @This(), ir: *const ModuleEnv, tree: *SExprTree, expr_idx: Self.Idx) std.mem.Allocator.Error!void {
        switch (self.*) {
            .e_num => |int_expr| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-num");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);

                var value_buf: [40]u8 = undefined;
                const value_str = int_expr.value.bufPrint(&value_buf) catch unreachable;
                try tree.pushStringPair("value", value_str);

                const attrs = tree.beginNode();
                try tree.endNode(begin, attrs);
            },
            .e_frac_f32 => |e| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-frac-f32");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);

                var value_buf: [512]u8 = undefined;
                const value_str = if (e.value == 0)
                    "0.0"
                else if (@abs(e.value) < 1e-10 or @abs(e.value) > 1e10)
                    std.fmt.bufPrint(&value_buf, "{e}", .{e.value}) catch "fmt_error"
                else
                    std.fmt.bufPrint(&value_buf, "{d}", .{e.value}) catch "fmt_error";
                try tree.pushStringPair("value", value_str);

                const attrs = tree.beginNode();
                try tree.endNode(begin, attrs);
            },
            .e_frac_f64 => |e| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-frac-f64");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);

                var value_buf: [512]u8 = undefined;
                const value_str = if (e.value == 0)
                    "0.0"
                else if (@abs(e.value) < 1e-10 or @abs(e.value) > 1e10)
                    std.fmt.bufPrint(&value_buf, "{e}", .{e.value}) catch "fmt_error"
                else
                    std.fmt.bufPrint(&value_buf, "{d}", .{e.value}) catch "fmt_error";
                try tree.pushStringPair("value", value_str);

                const attrs = tree.beginNode();
                try tree.endNode(begin, attrs);
            },
            .e_dec => |e| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-frac-dec");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);

                const dec_value_f64: f64 = @as(f64, @floatFromInt(e.value.num)) / std.math.pow(f64, 10, 18);
                var value_buf: [512]u8 = undefined;
                const value_str = if (dec_value_f64 == 0)
                    "0.0"
                else if (@abs(dec_value_f64) < 1e-10 or @abs(dec_value_f64) > 1e10)
                    std.fmt.bufPrint(&value_buf, "{e}", .{dec_value_f64}) catch "fmt_error"
                else
                    std.fmt.bufPrint(&value_buf, "{d}", .{dec_value_f64}) catch "fmt_error";
                try tree.pushStringPair("value", value_str);

                const attrs = tree.beginNode();
                try tree.endNode(begin, attrs);
            },
            .e_dec_small => |e| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-dec-small");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);

                var num_buf: [32]u8 = undefined;
                const num_str = std.fmt.bufPrint(&num_buf, "{}", .{e.value.numerator}) catch "fmt_error";
                try tree.pushStringPair("numerator", num_str);

                var denom_buf: [32]u8 = undefined;
                const denom_str = std.fmt.bufPrint(&denom_buf, "{}", .{e.value.denominator_power_of_ten}) catch "fmt_error";
                try tree.pushStringPair("denominator-power-of-ten", denom_str);

                const numerator_f64: f64 = @floatFromInt(e.value.numerator);
                const denominator_f64: f64 = std.math.pow(f64, 10, @floatFromInt(e.value.denominator_power_of_ten));
                const value_f64 = numerator_f64 / denominator_f64;

                var value_buf: [512]u8 = undefined;
                const value_str = if (value_f64 == 0)
                    "0.0"
                else if (@abs(value_f64) < 1e-10 or @abs(value_f64) > 1e10)
                    std.fmt.bufPrint(&value_buf, "{e}", .{value_f64}) catch "fmt_error"
                else
                    std.fmt.bufPrint(&value_buf, "{d}", .{value_f64}) catch "fmt_error";
                try tree.pushStringPair("value", value_str);

                const attrs = tree.beginNode();
                try tree.endNode(begin, attrs);
            },
            .e_str_segment => |e| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-literal");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);

                const value = ir.getString(e.literal);
                try tree.pushStringPair("string", value);

                const attrs = tree.beginNode();
                try tree.endNode(begin, attrs);
            },
            .e_str => |e| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-string");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();

                for (ir.store.sliceExpr(e.span)) |segment| {
                    try ir.store.getExpr(segment).pushToSExprTree(ir, tree, segment);
                }

                try tree.endNode(begin, attrs);
            },
            .e_list => |l| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-list");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();

                const elems_begin = tree.beginNode();
                try tree.pushStaticAtom("elems");
                const elems_attrs = tree.beginNode();
                for (ir.store.sliceExpr(l.elems)) |elem_idx| {
                    try ir.store.getExpr(elem_idx).pushToSExprTree(ir, tree, elem_idx);
                }
                try tree.endNode(elems_begin, elems_attrs);

                try tree.endNode(begin, attrs);
            },
            .e_empty_list => |_| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-empty_list");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();
                try tree.endNode(begin, attrs);
            },
            .e_tuple => |t| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-tuple");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();

                const elems_begin = tree.beginNode();
                try tree.pushStaticAtom("elems");
                const elems_attrs = tree.beginNode();
                for (ir.store.sliceExpr(t.elems)) |elem_idx| {
                    try ir.store.getExpr(elem_idx).pushToSExprTree(ir, tree, elem_idx);
                }
                try tree.endNode(elems_begin, elems_attrs);

                try tree.endNode(begin, attrs);
            },
            .e_lookup_local => |local| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-lookup-local");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();

                try ir.store.getPattern(local.pattern_idx).pushToSExprTree(ir, tree, local.pattern_idx);

                try tree.endNode(begin, attrs);
            },
            .e_lookup_external => |e| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-lookup-external");
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, e.region);
                const attrs = tree.beginNode();

                const module_idx_int = @intFromEnum(e.module_idx);
                std.debug.assert(module_idx_int < ir.imports.imports.items.items.len);
                const string_lit_idx = ir.imports.imports.items.items[module_idx_int];
                const module_name = ir.common.strings.get(string_lit_idx);
                // Special case: Builtin module is an implementation detail, print as (builtin)
                if (std.mem.eql(u8, module_name, "Builtin")) {
                    const field_begin = tree.beginNode();
                    try tree.pushStaticAtom("builtin");
                    const field_attrs = tree.beginNode();
                    try tree.endNode(field_begin, field_attrs);
                } else {
                    try tree.pushStringPair("external-module", module_name);
                }

                try tree.endNode(begin, attrs);
            },
            .e_lookup_required => |e| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-lookup-required");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();

                const requires_items = ir.requires_types.items.items;
                const idx = e.requires_idx.toU32();
                if (idx < requires_items.len) {
                    const required_type = requires_items[idx];
                    const ident_name = ir.getIdent(required_type.ident);
                    try tree.pushStringPair("required-ident", ident_name);
                }

                try tree.endNode(begin, attrs);
            },
            .e_match => |e| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-match");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();

                try e.pushToSExprTree(ir, tree, region);

                try tree.endNode(begin, attrs);
            },
            .e_if => |if_expr| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-if");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();

                const branches_begin = tree.beginNode();
                try tree.pushStaticAtom("if-branches");
                const branches_attrs = tree.beginNode();
                const branch_indices = ir.store.sliceIfBranches(if_expr.branches);
                for (branch_indices) |branch_idx| {
                    const branch = ir.store.getIfBranch(branch_idx);

                    const branch_begin = tree.beginNode();
                    try tree.pushStaticAtom("if-branch");
                    const branch_attrs = tree.beginNode();

                    try ir.store.getExpr(branch.cond).pushToSExprTree(ir, tree, branch.cond);
                    try ir.store.getExpr(branch.body).pushToSExprTree(ir, tree, branch.body);

                    try tree.endNode(branch_begin, branch_attrs);
                }
                try tree.endNode(branches_begin, branches_attrs);

                const else_begin = tree.beginNode();
                try tree.pushStaticAtom("if-else");
                const else_attrs = tree.beginNode();
                try ir.store.getExpr(if_expr.final_else).pushToSExprTree(ir, tree, if_expr.final_else);
                try tree.endNode(else_begin, else_attrs);

                try tree.endNode(begin, attrs);
            },
            .e_call => |c| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-call");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();

                const all_exprs = ir.store.exprSlice(c.args);

                try ir.store.getExpr(c.func).pushToSExprTree(ir, tree, c.func);

                if (all_exprs.len > 0) {
                    for (all_exprs[0..]) |arg_idx| {
                        try ir.store.getExpr(arg_idx).pushToSExprTree(ir, tree, arg_idx);
                    }
                }

                try tree.endNode(begin, attrs);
            },
            .e_record => |record_expr| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-record");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();

                if (record_expr.ext) |ext_idx| {
                    const ext_begin = tree.beginNode();
                    try tree.pushStaticAtom("ext");
                    const ext_attrs = tree.beginNode();
                    try ir.store.getExpr(ext_idx).pushToSExprTree(ir, tree, ext_idx);
                    try tree.endNode(ext_begin, ext_attrs);
                }

                const fields_begin = tree.beginNode();
                try tree.pushStaticAtom("fields");
                const fields_attrs = tree.beginNode();
                for (ir.store.sliceRecordFields(record_expr.fields)) |field_idx| {
                    try ir.store.getRecordField(field_idx).pushToSExprTree(ir, tree);
                }
                try tree.endNode(fields_begin, fields_attrs);

                try tree.endNode(begin, attrs);
            },
            .e_empty_record => |_| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-empty_record");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();
                try tree.endNode(begin, attrs);
            },

            .e_block => |block_expr| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-block");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();

                for (ir.store.sliceStatements(block_expr.stmts)) |stmt_idx| {
                    try ir.store.getStatement(stmt_idx).pushToSExprTree(ir, tree, stmt_idx);
                }

                try ir.store.getExpr(block_expr.final_expr).pushToSExprTree(ir, tree, block_expr.final_expr);

                try tree.endNode(begin, attrs);
            },
            .e_tag => |tag_expr| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-tag");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                try tree.pushStringPair("name", ir.getIdent(tag_expr.name));
                const attrs = tree.beginNode();

                if (tag_expr.args.span.len > 0) {
                    const args_begin = tree.beginNode();
                    try tree.pushStaticAtom("args");
                    const args_attrs = tree.beginNode();
                    for (ir.store.sliceExpr(tag_expr.args)) |arg_idx| {
                        try ir.store.getExpr(arg_idx).pushToSExprTree(ir, tree, arg_idx);
                    }
                    try tree.endNode(args_begin, args_attrs);
                }

                try tree.endNode(begin, attrs);
            },
            .e_nominal => |nominal_expr| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-nominal");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);

                const stmt = ir.store.getStatement(nominal_expr.nominal_type_decl);
                switch (stmt) {
                    .s_nominal_decl => |decl| {
                        const header = ir.store.getTypeHeader(decl.header);
                        try tree.pushStringPair("nominal", ir.getIdent(header.name));
                    },
                    else => {
                        // Handle malformed nominal type declaration by pushing error info
                        try tree.pushStringPair("nominal", "<malformed>");
                    },
                }

                const attrs = tree.beginNode();

                try ir.store.getExpr(nominal_expr.backing_expr).pushToSExprTree(ir, tree, nominal_expr.backing_expr);

                try tree.endNode(begin, attrs);
            },
            .e_nominal_external => |e| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-nominal-external");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();

                const module_idx_int = @intFromEnum(e.module_idx);
                std.debug.assert(module_idx_int < ir.imports.imports.items.items.len);
                const string_lit_idx = ir.imports.imports.items.items[module_idx_int];
                const module_name = ir.common.strings.get(string_lit_idx);
                // Special case: Builtin module is an implementation detail, print as (builtin)
                if (std.mem.eql(u8, module_name, "Builtin")) {
                    const field_begin = tree.beginNode();
                    try tree.pushStaticAtom("builtin");
                    const field_attrs = tree.beginNode();
                    try tree.endNode(field_begin, field_attrs);
                } else {
                    try tree.pushStringPair("external-module", module_name);
                }

                try ir.store.getExpr(e.backing_expr).pushToSExprTree(ir, tree, e.backing_expr);

                try tree.endNode(begin, attrs);
            },
            .e_zero_argument_tag => |zero_arg_tag_expr| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-zero-argument-tag");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                try tree.pushStringPair("closure", ir.getIdentText(zero_arg_tag_expr.closure_name));
                try tree.pushStringPair("name", ir.getIdentText(zero_arg_tag_expr.name));
                const attrs = tree.beginNode();
                try tree.endNode(begin, attrs);
            },
            .e_closure => |closure_expr| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-closure");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();

                // Display capture information if present
                if (closure_expr.captures.span.len > 0) {
                    const captures_begin = tree.beginNode();
                    try tree.pushStaticAtom("captures");
                    const captures_attrs = tree.beginNode();
                    for (ir.store.sliceCaptures(closure_expr.captures)) |captured_var_idx| {
                        const captured_var = ir.store.getCapture(captured_var_idx);
                        const capture_begin = tree.beginNode();
                        try tree.pushStaticAtom("capture");

                        const capture_region = ir.store.getPatternRegion(captured_var.pattern_idx);
                        try ir.appendRegionInfoToSExprTreeFromRegion(tree, capture_region);

                        try tree.pushStringPair("ident", ir.getIdentText(captured_var.name));
                        const capture_attrs = tree.beginNode();
                        try tree.endNode(capture_begin, capture_attrs);
                    }
                    try tree.endNode(captures_begin, captures_attrs);
                }

                try ir.store.getExpr(closure_expr.lambda_idx).pushToSExprTree(ir, tree, closure_expr.lambda_idx);

                try tree.endNode(begin, attrs);
            },
            .e_lambda => |lambda_expr| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-lambda");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();

                const args_begin = tree.beginNode();
                try tree.pushStaticAtom("args");
                const args_attrs = tree.beginNode();
                for (ir.store.slicePatterns(lambda_expr.args)) |arg_idx| {
                    try ir.store.getPattern(arg_idx).pushToSExprTree(ir, tree, arg_idx);
                }
                try tree.endNode(args_begin, args_attrs);

                try ir.store.getExpr(lambda_expr.body).pushToSExprTree(ir, tree, lambda_expr.body);

                try tree.endNode(begin, attrs);
            },
            .e_binop => |e| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-binop");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                try tree.pushStringPair("op", @tagName(e.op));
                const attrs = tree.beginNode();

                try ir.store.getExpr(e.lhs).pushToSExprTree(ir, tree, e.lhs);
                try ir.store.getExpr(e.rhs).pushToSExprTree(ir, tree, e.rhs);

                try tree.endNode(begin, attrs);
            },
            .e_unary_minus => |e| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-unary-minus");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();

                try ir.store.getExpr(e.expr).pushToSExprTree(ir, tree, e.expr);

                try tree.endNode(begin, attrs);
            },
            .e_unary_not => |e| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-unary-not");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();

                try ir.store.getExpr(e.expr).pushToSExprTree(ir, tree, e.expr);

                try tree.endNode(begin, attrs);
            },
            .e_dot_access => |e| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-dot-access");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                try tree.pushStringPair("field", ir.getIdentText(e.field_name));
                const attrs = tree.beginNode();

                const receiver_begin = tree.beginNode();
                try tree.pushStaticAtom("receiver");
                const receiver_attrs = tree.beginNode();
                try ir.store.getExpr(e.receiver).pushToSExprTree(ir, tree, e.receiver);
                try tree.endNode(receiver_begin, receiver_attrs);

                if (e.args) |args| {
                    const args_begin = tree.beginNode();
                    try tree.pushStaticAtom("args");
                    const args_attrs = tree.beginNode();
                    for (ir.store.exprSlice(args)) |arg_idx| {
                        try ir.store.getExpr(arg_idx).pushToSExprTree(ir, tree, arg_idx);
                    }
                    try tree.endNode(args_begin, args_attrs);
                }

                try tree.endNode(begin, attrs);
            },
            .e_runtime_error => |e| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-runtime-error");

                const diagnostic = ir.store.getDiagnostic(e.diagnostic);
                const msg = try std.fmt.allocPrint(ir.gpa, "{s}", .{@tagName(diagnostic)});
                defer ir.gpa.free(msg);

                try tree.pushStringPair("tag", msg);

                const attrs = tree.beginNode();
                try tree.endNode(begin, attrs);
            },
            .e_ellipsis => |_| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-not-implemented");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();
                try tree.endNode(begin, attrs);
            },
            .e_anno_only => |_| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-anno-only");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();
                try tree.endNode(begin, attrs);
            },
            .e_hosted_lambda => |hosted| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-hosted-lambda");
                const symbol_name = ir.common.getIdent(hosted.symbol_name);
                try tree.pushStringPair("symbol", symbol_name);
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();

                const args_begin = tree.beginNode();
                try tree.pushStaticAtom("args");
                const args_attrs = tree.beginNode();
                for (ir.store.slicePatterns(hosted.args)) |arg_idx| {
                    try ir.store.getPattern(arg_idx).pushToSExprTree(ir, tree, arg_idx);
                }
                try tree.endNode(args_begin, args_attrs);

                try tree.endNode(begin, attrs);
            },
            .e_low_level_lambda => |low_level| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-low-level-lambda");
                const op_name = try std.fmt.allocPrint(ir.gpa, "{s}", .{@tagName(low_level.op)});
                defer ir.gpa.free(op_name);
                try tree.pushStringPair("op", op_name);
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();

                const args_begin = tree.beginNode();
                try tree.pushStaticAtom("args");
                const args_attrs = tree.beginNode();
                for (ir.store.slicePatterns(low_level.args)) |arg_idx| {
                    try ir.store.getPattern(arg_idx).pushToSExprTree(ir, tree, arg_idx);
                }
                try tree.endNode(args_begin, args_attrs);

                try ir.store.getExpr(low_level.body).pushToSExprTree(ir, tree, low_level.body);

                try tree.endNode(begin, attrs);
            },
            .e_crash => |e| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-crash");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                try tree.pushStringPair("msg", ir.getString(e.msg));
                const attrs = tree.beginNode();
                try tree.endNode(begin, attrs);
            },
            .e_dbg => |e| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-dbg");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();

                try ir.store.getExpr(e.expr).pushToSExprTree(ir, tree, e.expr);

                try tree.endNode(begin, attrs);
            },
            .e_inspect => |e| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-inspect");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();

                try ir.store.getExpr(e.expr).pushToSExprTree(ir, tree, e.expr);

                try tree.endNode(begin, attrs);
            },
            .e_expect => |expect_expr| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-expect");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();

                // Add body expression
                try ir.store.getExpr(expect_expr.body).pushToSExprTree(ir, tree, expect_expr.body);

                try tree.endNode(begin, attrs);
            },
            .e_return => |ret| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-return");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();

                // Add inner expression
                try ir.store.getExpr(ret.expr).pushToSExprTree(ir, tree, ret.expr);

                try tree.endNode(begin, attrs);
            },
            .e_for => |for_expr| {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("e-for");
                const region = ir.store.getExprRegion(expr_idx);
                try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
                const attrs = tree.beginNode();

                // Add pattern
                try ir.store.getPattern(for_expr.patt).pushToSExprTree(ir, tree, for_expr.patt);

                // Add list expression
                try ir.store.getExpr(for_expr.expr).pushToSExprTree(ir, tree, for_expr.expr);

                // Add body expression
                try ir.store.getExpr(for_expr.body).pushToSExprTree(ir, tree, for_expr.body);

                try tree.endNode(begin, attrs);
            },
        }
    }

    /// Pattern matching expression that destructures values and executes different branches.
    /// Match expressions are exhaustive - they must handle all possible cases of the matched value.
    /// Each branch consists of one or more patterns, an optional guard condition, and a result expression.
    ///
    /// ```roc
    /// match result {
    ///     Ok(value) => value,
    ///     Err(msg) => crash("Error: ${msg}"),
    /// }
    ///
    /// match shape {
    ///     Circle(radius) if radius > 0 => 3.14 * radius * radius,
    ///     Circle(_) => 0,
    ///     Rectangle(w, h) => w * h,
    ///     Square(side) => side * side,
    /// }
    /// ```
    pub const Match = struct {
        /// The condition of the match expression.
        cond: Expr.Idx,
        /// The branches of the `match`
        branches: Branch.Span,
        /// Marks whether a match expression is exhaustive using a variable.
        exhaustive: TypeVar,

        pub const Idx = enum(u32) { _ };
        pub const Span = extern struct { span: base.DataSpan };

        /// A single branch within a match expression.
        /// Contains patterns to match against, an optional guard condition,
        /// and the expression to evaluate when the patterns match.
        ///
        /// ```roc
        /// # This branch has a single pattern `Ok(value)` and expression `value`
        /// Ok(value) => value
        ///
        /// # This branch has pattern `x` with guard `x > 0` and expression `"positive"`
        /// x if x > 0 => "positive"
        /// ```
        pub const Branch = struct {
            patterns: Match.BranchPattern.Span,
            value: Expr.Idx,
            guard: ?Expr.Idx,
            /// Marks whether a match branch is redundant using a variable.
            redundant: TypeVar,

            pub fn pushToSExprTree(self: *const Match.Branch, ir: *const ModuleEnv, tree: *SExprTree, _: Match.Branch.Idx) std.mem.Allocator.Error!void {
                const begin = tree.beginNode();
                try tree.pushStaticAtom("branch");
                const attrs = tree.beginNode();

                const patterns_begin = tree.beginNode();
                try tree.pushStaticAtom("patterns");
                const patterns_attrs = tree.beginNode();
                const patterns_slice = ir.store.sliceMatchBranchPatterns(self.patterns);
                for (patterns_slice) |branch_pat_idx| {
                    const branch_pat = ir.store.getMatchBranchPattern(branch_pat_idx);
                    const pattern_begin = tree.beginNode();
                    try tree.pushStaticAtom("pattern");
                    try tree.pushBoolPair("degenerate", branch_pat.degenerate);
                    const pattern_attrs = tree.beginNode();
                    const pattern = ir.store.getPattern(branch_pat.pattern);
                    try pattern.pushToSExprTree(ir, tree, branch_pat.pattern);
                    try tree.endNode(pattern_begin, pattern_attrs);
                }
                try tree.endNode(patterns_begin, patterns_attrs);

                const value_begin = tree.beginNode();
                try tree.pushStaticAtom("value");
                const value_attrs = tree.beginNode();
                try ir.store.getExpr(self.value).pushToSExprTree(ir, tree, self.value);
                try tree.endNode(value_begin, value_attrs);

                if (self.guard) |guard_idx| {
                    const guard_begin = tree.beginNode();
                    try tree.pushStaticAtom("guard");
                    const guard_attrs = tree.beginNode();
                    try ir.store.getExpr(guard_idx).pushToSExprTree(ir, tree, guard_idx);
                    try tree.endNode(guard_begin, guard_attrs);
                }

                try tree.endNode(begin, attrs);
            }

            pub const Idx = enum(u32) { _ };
            pub const Span = extern struct { span: DataSpan };
        };

        /// A pattern within a match branch, which may be part of an OR pattern.
        /// Multiple patterns in a single branch are separated by `|`.
        /// Each pattern can independently match the scrutinee value.
        ///
        /// ```roc
        /// match value {
        ///     # Single pattern in branch
        ///     Some(x) => x,
        ///     # Multiple patterns in branch using bar separator `|`
        ///     None | Empty => 0,
        /// }
        /// ```
        pub const BranchPattern = struct {
            pattern: CIR.Pattern.Idx,
            /// Degenerate branch patterns are those that don't fully bind symbols that the branch body
            /// needs. For example, in `A x | B y -> x`, the `B y` pattern is degenerate.
            /// Degenerate patterns emit a runtime error if reached in a program.
            degenerate: bool,

            pub const Idx = enum(u32) { _ };
            pub const Span = extern struct { span: base.DataSpan };
        };

        pub fn pushToSExprTree(self: *const @This(), ir: *const ModuleEnv, tree: *SExprTree, region: Region) std.mem.Allocator.Error!void {
            const begin = tree.beginNode();
            try tree.pushStaticAtom("match");
            try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
            const attrs = tree.beginNode();

            const cond_begin = tree.beginNode();
            try tree.pushStaticAtom("cond");
            const cond_attrs = tree.beginNode();
            try ir.store.getExpr(self.cond).pushToSExprTree(ir, tree, self.cond);
            try tree.endNode(cond_begin, cond_attrs);

            const branches_begin = tree.beginNode();
            try tree.pushStaticAtom("branches");
            const branches_attrs = tree.beginNode();
            for (ir.store.matchBranchSlice(self.branches)) |branch_idx| {
                try ir.store.getMatchBranch(branch_idx).pushToSExprTree(ir, tree, branch_idx);
            }
            try tree.endNode(branches_begin, branches_attrs);

            try tree.endNode(begin, attrs);
        }
    };

    /// Represents a variable captured by a lambda
    pub const Capture = struct {
        name: base.Ident.Idx,
        pattern_idx: CIR.Pattern.Idx,
        scope_depth: u32,

        pub const Idx = enum(u32) { _ };
        pub const Span = extern struct { span: base.DataSpan };
    };
};
